Priority Tax ReliefDesign System Consumption & Source-of-Truth Plan · v2 · reconciled July 30, 2026

PTR Design Kit

Making the design system the company's source of truth

A plan to ingest the PTR Design Kit into the Priority Hub app and standardize it as the de-facto system across every PTR surface — with one versioned package as the single source of truth, and the brand/deck assets kept fully usable outside the app. Phase 1 is closed: v0.2.0 tagged July 22, and distribution moved the same day to jsDelivr off the public repo — no registry, no auth; the files serve, and Phase 2 (the Priority Hub app swap) is next.

Prepared
July 14, 2026
Distribution
jsDelivr CDN · public repo, tag-pinned
App integration
Re-theme, zero template changes
Status
Phase 1 · complete · v0.2.0 tag on jsDelivr (July 22) · docs reconciled July 30
Next
Phase 2 — Priority Hub app swap

The core insight

The Priority Hub app and this design system already speak the same language. The app is built on Tailwind v4 + the DaisyUI plugin (input.cssoutput.css), and its live theme is declared in og_dashboard.css as [data-theme="light"] / [data-theme="dark"] blocks. Those blocks set the exact same CSS variables our ptr-theme.css defines — --color-primary, --color-base-100, --radius-box, and so on. Today they hold an Apple-style blue palette (#0071e3); our theme holds PTR Green (#086033).

What this means

Consuming the design system is not a rebuild — it is a theme swap. Because the app's component CSS already reads DaisyUI tokens (not hardcoded hex), replacing the variable values re-skins the entire app to PTR. Every button, badge, card, sidebar and leaderboard medal inherits the brand the moment the tokens change. This is the whole reason DaisyUI was chosen, and it's why "source of truth" is achievable cheaply.

The source-of-truth chain

One directional flow, no parallel copies. Figma is where design decisions are made; a single published package is what everything installs. Nothing hand-maintains a second copy of the tokens.

OriginFigma "PTR" themeVariables edited by design; exported as tokens JSON
Truth filePTR.tokens.jsonDaisyUI v5 theme export — the one canonical token set
BuildGenerated CSStheme + brand + component layers, from the JSON
ShipOne tagged packagePublic repo · served tag-pinned by jsDelivr
ConsumeEvery surfaceApp, landing pages, decks, HubSpot, Ads

Governance — how it stays the source of truth

The package: one package, everything in it

Per your call, we ship a single scoped package and let each consumer link only the layer it needs. Served from the public design-kit repo via jsDelivr — no registry, no auth anywhere; every consumer pins a v* tag in the URL. (Revised July 22: GitHub Packages dropped before any consumer installed.)

@prioritytaxrelief-development/design-kit — exports map
// package.json "exports" — consumers cherry-pick their layer
{
  "./theme.css":      // DaisyUI "PTR" theme — the tokens the app needs
  "./theme.plugin.css": // @plugin block for Tailwind+DaisyUI builds
  "./theme-dark.css":  // NEW (July 16) — branded dark theme, selector-gated (D5)
  "./brand.css":      // extensions: brand palette, case-status, type scale
  "./marketing.css"   // + "./pages.css" — marketing/web layer, canonical July 17
  "./product.css":    // shared product layer — shipped in v0.2 (July 17)
  "./tokens.json":    // + "./tokens.dark.json" — the source-of-truth token sets
  "./assets/*"         // logo lockups, textures, fonts config
}
Core / app layer

theme.css + theme.plugin.css

The DaisyUI "PTR" theme. This is all the Priority Hub app strictly needs — it re-skins every DaisyUI component.

Extension layer

brand.css

The 8 case-status colors, extended brand palette, type scale, spacing and motion tokens DaisyUI has no slot for — plus (July 16) layout tokens (--site-max/--gutter/--header-h), surfaces (--surface-1/2) and the Inter eyebrow pair (.ptr-eyebrow quiet/--loud).

Component layer

product.css

The shared product vocabulary — status pills (--case-{group}-bg/-fg), empty states, filter bars, toasts. Deferred from v0.1, then shipped in v0.2 (July 17) under budget rule 2: vocabulary used by 2+ apps lives here, never copied per app. App-bespoke chrome stays app-side.

Marketing layer

marketing.css (+ pages.css)

The Services-site web vocabulary — header/nav, hero, section bands, service cards, accordions, lead form, footer, dark theme. Same tokens, no DaisyUI dependency; the style contract for the Django/Wagtail rebuild. Packaged July 17: canonical copies now ship as package/marketing.css + pages.css; ui_kits/marketing/ re-exports them.

Brand / deck layer

assets + brand.css

Logos, grain texture, fonts and the brand palette — everything the marketing/deck side pulls without touching the app.

Integrating into the Priority Hub app

You chose to replace the app's light / dark theme values with PTR — which means zero changes to templates or JavaScript. The Alpine binding :data-theme="darkMode ? 'dark' : 'light'" stays exactly as it is; we only change what those two theme names resolve to.

  1. Link the package

    No install — three tag-pinned jsDelivr <link>s in the base template, after the compiled output.css: theme.css, brand.css, theme-dark.css.

  2. Leave input.css alone

    @plugin "daisyui" { themes: light, dark } stays — the built-ins remain registered as a fallback. Loading after output.css, theme.css overrides light to PTR and theme-dark.css re-flips dark to the branded dark. Theme names unchanged, so the Alpine binding is untouched.

  3. Delete the app's inline palette overrides

    Remove the [data-theme="light"] / [data-theme="dark"] variable blocks from og_dashboard.css. They become redundant — the package now owns those values. All the app-specific component rules below them stay untouched.

  4. Fonts (+ brand layer already linked)

    brand.css ships in the step-1 link stack, so case-status colors and the type scale are available app-wide. Swap the Google Fonts <link> for the package's font config to guarantee the exact Inter weights.

  5. Rebuild & verify

    npm run build:css. The dashboard renders in PTR Green with no template edits. Spot-check contrast on badges and the leaderboard (audit item #8).

priority_hub/templates/base.html — after the compiled output.css
<link rel="stylesheet" href="{% static 'css/output.css' %}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/PriorityTaxRelief-Development/design-kit@v0.2.0/package/theme.css">       // PTR tokens → override "light"
<link rel="stylesheet" href="…/design-kit@v0.2.0/package/brand.css">       // extension tokens + .ptr-* classes
<link rel="stylesheet" href="…/design-kit@v0.2.0/package/theme-dark.css">  // branded dark → re-flips "dark"
priority_hub/static/css/og_dashboard.css — remove these, keep everything else
[data-theme="light"] { --color-primary: #0071e3; --color-base-100: #fff; … }
[data-theme="dark"]  { --color-primary: #2997ff; … }
/* ✓ keep: .og-sidebar, .og-lb-card, .dispo-chip, all component rules —
   they read var(--color-*) and now inherit PTR automatically */

Consumption playbook — every other surface

Same package, same entry point everywhere: tag-pinned jsDelivr links — identical URLs whether or not the platform can run a build (jsDelivr auto-serves minified .min.css variants). Repos that want the theme registered at build time vendor theme.plugin.css from the tagged URL. The brand/deck side stays independent by design: it consumes only brand.css (palette, type, logos, textures) and the standalone slides/ templates — never the DaisyUI component layer — so a product CSS change can never break a slide, and a deck asset can never leak into the product bundle. One source of truth, two clean audiences.

CDN link · jsDelivr

Priority Hub app

  • Link the package, re-theme light
  • Zero template/JS changes
  • Primary consumer
CDN link · jsDelivr

Landing pages / Services site

  • Django/Wagtail + Tailwind build
  • Link theme + brand + marketing
  • Vocabulary blocks map 1:1 to StreamField block templates; header/footer as theme partials
Drop-in · CDN CSS

HubSpot

  • Link the same jsDelivr URLs in the template head (auto-minified .min.css)
  • Tag-pinned — same as every other surface
Drop-in · CDN CSS

Google Ads platform

  • Same jsDelivr links
  • Use tokens for any custom ad/landing UI
Bridge

Figma sync

  • tokens.json is the two-way bridge (Tokens Studio / Figma variables)
  • Design edits → JSON → package release
Assets only

Brand decks & marketing

  • Pull brand.css + assets/
  • Slides in slides/ stay standalone
  • Never touches the app theme

Marketing layer — discrepancy & open-issues log

Audit of the Services project (July 15, 2026) against the system: token coverage is solid — its ptr-tokens.css matches the package exactly, and both site stylesheets use zero hardcoded brand hex. But the site made five design decisions the system doesn't cover. They were ingested verbatim into ui_kits/marketing/ and logged here — decided & executed July 16, 2026. This log now also tracks the audit's open items (#). Resolved rows are grayed; open rows need your call.

#DiscrepancySite saysSystem saysDecision · status
D1typeMarketing type scale17px body; responsive clamp() headings at weight 800–900Fixed-px 1920-grid deck scale (--fs-*), weight 400Resolved — system style. Body → --fs-body/--lh-body; display headings weight 400 with token-capped clamps (--fs-h1/h2/h3/display). Component titles kept at 800 → outlier O1; mid sizes → O2; px line-heights → O3. Follow-up July 16: the original weight-800 display headings return as an opt-in variation — body[data-headings="heavy"] in the marketing layer; weight 400 stays the standard.
D2typeEyebrow style.eyebrow: Inter 700 · 13px · orange.ptr-eyebrow: Outfit · 12px · graphiteResolved — converged on Inter. Quiet (.ptr-eyebrow / .eyebrow--quiet: graphite 600·12) + loud (.ptr-eyebrow--loud / .eyebrow: orange 700·13), both in brand.css. Outfit stays for measure/spec labels only.
D3layoutLayout tokens defined locally--site-max: 1240px, --gutter: 40px, --header-h: 84pxNo layout tokens existResolved — promoted. All three now in package/brand.css; the marketing-local :root block is gone. Figma-side variables pending the next tokens.json sync (O5).
D4motionMotion valuesOwn --ease: cubic-bezier(.22,.61,.36,1); literal 80/120/140/180ms--ease-out/--ease-in-out, --duration-press/fast/base/slow (80/120/180/320) — added after the site shippedResolved — system tokens. All 77 transitions on --duration-* + --ease-out: 80→press, 120/140→fast, 160–240→base, 300→slow. The 600ms entrance reveal stays literal → O4.
D5themeDark modeFull hand-rolled html[data-theme="dark"] token flip: --surface-1/2, brightened green, light logo chips — WCAG AA, shippedNo dark theme (audit #12: neutral placeholder)Resolved — promoted. New package/theme-dark.css ([data-theme="dark"] / [data-theme="ptr-dark"]) holds the token flip incl. --surface-1/2; marketing cards/inputs are now surface-token driven (~15 dark overrides deleted, undefined --text-1/--border-1 refs killed). tokens.json fold-in pending Figma dark mode (O5).
O1typeComponent-title weightCard/job/post titles: 19–24px at weight 800Display type now weight 400; system h4 weight is 600Resolved — kept 800, made a style. Sanctioned as .ptr-title (--fs-title 21 / --lh-title 1.25) in package/brand.css; canonical card/job titles now reference the token.
O2typeMid-size text gapWeb copy needs 17–22px roles (leads 20–22, intros 19, prose 18)Scale jumps 16 → 28 — no slot between body and leadResolved — tokens added. --fs-lead-web 20 · --fs-intro 19 · --fs-body-lg 18 (+ --fs-title 21, O1), each with a unitless --lh-*; marketing layer consumes them (hero leads, intros, prose, bands).
O3typepx line-height tokensMarketing uses unitless equivalents under responsive clamp()--lh-h2/h3/lead/display are fixed px off the 1920 deck gridResolved — unitless. Re-expressed as ratios (98/96 → 1.0208, 46/42 → 1.0952, 34/28 → 1.2143) — deck output unchanged; marketing .section h2 now uses --lh-h2 directly.
O4motion600ms entrance revealScroll-entrance reveal animates at 600ms (kept literal)Duration tokens cap at --duration-slow 320msResolved — existing token. The reveal now runs on --duration-slow (320ms) — no new token; the last literal duration in the marketing layer is gone.
O5tokensFigma sync debtLayout tokens (D3) + dark theme (D5) hand-authored in the packagetokens.json is a single-mode export with no layout group — build.mjs doesn't own themResolved — folded in. tokens.json gains color.brand (12 Pantone-mapped) + color.surface + layout groups; new tokens.dark.json carries the dark mode (only the variables that flip). build.mjs now generates theme-dark.css and the brand/layout/surface blocks in brand.css — ready to import into Figma.
#8a11yAccessibility / contrastPTR Orange used decoratively across surfacesNo text-vs-fill rule; orange must never carry body text on whiteResolved — rule set July 16. Orange is a selective accent: never small text (loud eyebrows, card/post links, hover links → green; form-error micro-text → the error pair); large heavy display headlines may be orange (hero accents, stat suffixes stay); as a fill only on the key CTA, always with dark text (buttons already comply; ::selection fixed). Documented in package/brand.css. Badge + leaderboard contrast spot-check remains in the Phase-2 app swap. Documented July 30 — audit #4 closed: every sanctioned pairing measured (README Accessibility & contrast + two live-computing preview cards). Two exported feedback pairs came back under AA for normal text → new item 17.
#17a11y · tokensFeedback pairs under AAsuccess-content on success = 3.58:1; info-content on info = 4.45:1 — straight from the Figma exportAA needs 4.5:1 for normal textOpen — needs a Figma edit. Darken the two -content values in the "PTR" theme, re-export tokens.json, npm run build. Not hand-fixable: token values only enter through the build. Documented as fills/large-text-only until then; status text uses the --case-* pill pairs, which all clear AA.
#2/#5bundleBundle hygieneStatusBadge cross-file dependency; demo fixtures shipped in the public bundlePublish-blocker for the first releaseResolved — July 15. StatusBadge extracted with a declared dependency; fixtures module-local; CaseList takes a cases prop.
#6assetsHeavy grain assetBrand layer shipped a ~8 MB grain texturePackage must stay leanResolved — July 15. Compressed 116 KB grain.jpg ships; full-res master stays repo-only.
#12themeBranded dark themeNeutral dark placeholder in the appAudit called for a branded dark pass (was Phase 3)Resolved — July 16. Landed early as package/theme-dark.css via D5; tokens.json fold-in tracked as O5. Neutral dark dropped from the app plan (July 16, later) — dark resolves to the branded theme from day one.

Also pendingproduct.css — component layer, deferred by decision (July 15); revived for v0.2 as the shared product layer (July 17, custom-CSS budget rule 2): vocabulary used by 2+ apps (status pills, empty states, filter bars, toasts) ships here, never copied per app.

Rollout in three phases

PhaseWhat shipsOutcome
PackagePhase 1 · foundation · shipped July 22Restructured into the publishable package: token build from tokens.json, exports map, CI release workflow. v0.2.0 tagged in the design-kit repo — made public July 22, served tag-pinned by jsDelivr (distribution revised from GitHub Packages the same day).A versioned single source of truth exists and serves.
App swapPhase 2 · Priority Hub · nextRe-theme light to PTR, delete inline overrides, load brand layer, verify contrast. Close audit items #8 (contrast) and #2/#5 (bundle hygiene).The flagship app is fully PTR-branded, zero template rewrites.
Fan-outPhase 3 · everywhereLanding pages onto the package; HubSpot + Ads onto the same jsDelivr links (auto-minified); wire the Figma ↔ tokens.json sync; branded dark theme (#12 — landed early, July 16, via package/theme-dark.css).De-facto system across every PTR surface.

Worklog — Phase 1

Running record of the build-out, updated as steps land. Last update July 22, 2026. Phase 1 closed with the July 22 release.

Repo inventory — what lives where

Four buckets. Only package/ ships to consumers; everything else either powers this workspace or documents the work. Audited July 15, 2026 — nothing unaccounted for remains at root.

BucketFilesRule
Workspace machineryClaude environment_ds_bundle.js, _ds_manifest.json, _adherence.oxlintrc.json, thumbnail.html, styles.css (compiler entry), doc-page.js (renders this doc), templates/*/support.js · ds-base.js · deck-stage.jsAuto-generated or runtime scaffolding — never hand-edit, never publish.
Design systemconsumed hereREADME.md, SKILL.md, ptr-theme.css + colors_and_type.css (wire the package in), preview/ (30 cards; 42 registered cards in total once slides + UI kits are counted), slides/, templates/, ui_kits/product/ + ui_kits/marketing/ (the ingested Services-site web vocabulary), assets/ (working masters: full-res grain, team portrait — logos live only in the package; assets/logo-kit/ holds the official high-res PNG/favicon/BIMI/cover exports from ptr_logo for the Downloads page)The system's docs, specimens and components. Reference — not shipped.
The packageserved via jsDelivrpackage/ only: tokens.json + tokens.dark.json (truth) → build.mjstheme.css / theme.plugin.css / theme-dark.css / brand.css, CHANGELOG.md, hand-authored marketing.css + pages.css (canonical, July 17) + product.css (shared product layer, v0.2), assets/ (logos + 116 KB grain) and package.json / README.mdThe only publishable surface. Token edits enter via tokens.json alone.
Working docsproject managementConsumption Plan.html (this doc), Product Surface Plan.html, AUDIT_PLAN.md, design_handoff_phase2_app_swap/ (the engineering handoff — links the package, never copies it)Living records — updated as phases land. Rule: a doc states a fact once and links for the rest; no doc restates a token value or a file's contents.